﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using App.Core.Extensions;
//using Lucene.Net.Index;
using Lucene.Net.Search;
using Lucene.Net.Documents;
using Lucene.Net.Store;
using Lucene.Net.QueryParsers;
using Lucene.Net.Analysis;
using Lucene.Net.Index;

namespace App.Search
{
    public class ProductSearchManager
    {
        private static IndexSearcher _IndexSearcher = null;

        /// <summary>
        /// Reloads the Searcher Index. Searcher should be reloaded 
        /// whenever something is changed in the physical index at filesystem.
        /// </summary>
        public static void Reload()
        {
            if (_IndexSearcher != null)
            {
                if (String.IsNullOrEmpty(SearchConstants.ProductIndexDirectory))
                    throw new ArgumentException("Lucene Index Directory for Products Cannot be Empty");

                if (!System.IO.Directory.Exists(SearchConstants.ProductIndexDirectory))
                    throw new ArgumentException(String.Format("Lucene Product Index Directory {0} does not exist", SearchConstants.ProductIndexDirectory));

                _IndexSearcher.Dispose();                
                Directory directory = FSDirectory.Open(SearchConstants.ProductIndexDirectory);
                _IndexSearcher = new IndexSearcher(directory);
            }
        }
        #region Constructors
        public ProductSearchManager() 
        {            
            InitIndexReader(SearchConstants.ProductIndexDirectory);
        }
        /// <summary>
        /// Initializer for the Lucene Indexer.
        /// </summary>
        /// <param name="indexDirectory">Index directory where Lucene will store its index in the physical disk.</param>
        //public ProductSearchManager(String indexDirectory)
        //{
        //    InitIndexReader(indexDirectory);            
        //}
        /// <summary>
        /// Initializes Index Searcher
        /// </summary>
        /// <param name="indexDirectory"></param>
        private void InitIndexReader(String indexDirectory)
        {
            if (String.IsNullOrEmpty(indexDirectory))
                throw new ArgumentException("Lucene Index Directory for Products Cannot be Empty");

            if (!System.IO.Directory.Exists(indexDirectory))
                throw new ArgumentException(String.Format("Lucene Product Index Directory {0} does not exist", indexDirectory));


            if (_IndexSearcher == null)
            {
                Directory directory = FSDirectory.Open(indexDirectory);
                _IndexSearcher = new IndexSearcher(directory);
            }
            //return _IndexSearcher;
        }
        #endregion Constructors

        #region Public Methods

        public String Search(String keyword, int pageSize, int pageNo, out int totalRecord)
        {
            BooleanQuery query = new BooleanQuery();
            BuildQuery(keyword, query);
            //keyword = keyword.ToUpper();
            ///Multifield Query Parser, to search in multipile field
            //MultiFieldQueryParser queryParser = new MultiFieldQueryParser(Lucene.Net.Util.Version.LUCENE_30
            //                    , new String[] { "company", "category", "model", "title", "description", }
            //                    , new WhitespaceAnalyzer());
            //PhraseQuery phraseQuery = new PhraseQuery();
            //QueryParser queryParser = new QueryParser(Lucene.Net.Util.Version.LUCENE_30, "title", new WhitespaceAnalyzer());            
            //query.Add(queryParser.Parse(parsedKeyword), Occur.SHOULD);
            //query.Add(new FuzzyQuery(new Term("title", keyword)), Occur.SHOULD);

            ///Perform Search
            TopDocs docs = _IndexSearcher.Search(query, pageSize * pageNo);
            totalRecord = docs.TotalHits;
            return GetProductIDs(docs, pageNo, pageSize);
        }

        /// <summary>
        /// Refine Search Results
        /// </summary>
        /// <param name="payload"></param>
        /// <param name="pageSize"></param>
        /// <param name="pageNo"></param>
        /// <param name="totalRecord"></param>
        /// <returns></returns>
        public string RefineSearch(SearchPayload payload, int pageSize, int pageNo, out int totalRecord)
        {
            BooleanQuery query = new BooleanQuery();
            BuildQuery(payload.Keyword, query);            
            ///Process Custom Filters
            BooleanQuery mergedQuery = new BooleanQuery();
            mergedQuery.Add(query, Occur.MUST);
            BooleanQuery multiSelectQuery = new BooleanQuery();
            foreach (SearchFilter filter in payload.Filters)
            {
                if (!filter.Value.IsNullOrEmpty() && !filter.SearchField.IsNullOrEmpty())
                {
                    ///Process Range Query
                    if (String.Compare(filter.SearchType, "Range", true) == 0)
                    {
                        String[] values = filter.Value.Split('-');                        
                        double? fromRange = values[0].ToDouble();
                        double? toRange = null; //int.MaxValue;//612345;
                        if(values.Length > 1 && !values[1].IsNullOrEmpty())
                            toRange = values[1].ToDouble();
                        Query rangeQuery = NumericRangeQuery.NewDoubleRange(filter.SearchField.ToLower(), fromRange, toRange, true, true);
                        mergedQuery.Add(rangeQuery, Occur.MUST);
                        //RangeQuery rangeQuery = new RangeQuery();                        
                    }
                    ///Process Normal Term Query
                    else
                    {
                        QueryParser queryParser = new QueryParser(Lucene.Net.Util.Version.LUCENE_30, filter.SearchField.ToLower(), new WhitespaceAnalyzer());
                        queryParser.DefaultOperator = QueryParser.Operator.AND;
                        multiSelectQuery.Add(queryParser.Parse(filter.Value.ToUpper()), Occur.SHOULD);

                        //Query prefixQuery = new PrefixQuery(new Term(filter.SearchField.ToLower(), filter.Value.ToUpper()));
                        //mergedQuery.Add(prefixQuery, Occur.MUST);

                        //TermQuery termQuery = new TermQuery(new Term(filter.SearchField.ToLower(), filter.Value.ToUpper()));
                        //mergedQuery.Add(termQuery, Occur.MUST);
                    }
                }
            }
            if (multiSelectQuery.Count() > 0)
                mergedQuery.Add(multiSelectQuery, Occur.MUST);
            //query.Add(new BooleanClause(innerQuery, Occur.MUST));
            TopDocs docs = _IndexSearcher.Search(mergedQuery, pageSize * pageNo);
            totalRecord = docs.TotalHits;
            return GetProductIDs(docs, pageNo, pageSize);
        }
        /// <summary>
        /// Parses a User Given Keyword to be understandable by the Lucene QueryParsers and Adds necessary query objects accordingly
        /// <para>TODO: Need to Add Support for Wildcard Queries for multi word keyword, to search similar to starts with or contains</para>
        /// </summary>
        /// <param name="keyword"></param>
        /// <param name="query"></param>        
        private void BuildQuery(String keyword, BooleanQuery query)
        {
            keyword = keyword.StripExtraSpace().Trim().ToUpper();
            QueryParser queryParser = new QueryParser(Lucene.Net.Util.Version.LUCENE_30, "full_content", new WhitespaceAnalyzer());
            if (IsExactSearch(ref keyword))
            {                
                queryParser.AllowLeadingWildcard = true;
                queryParser.DefaultOperator = QueryParser.Operator.AND;
                query.Add(queryParser.Parse(keyword), Occur.SHOULD);
            }
            else if (keyword.WordCount() == 1)
            {
                keyword = QueryParser.Escape(keyword);
                //query.Add(queryParser.Parse(keyword), Occur.SHOULD);
                //query.Add(new TermQuery(new Term("full_content", keyword)), Occur.SHOULD);
                //query.Add(new WildcardQuery(new Term("model", String.Format("*{0}*", keyword))), Occur.SHOULD);
                //query.Add(new WildcardQuery(new Term("title", String.Format("*{0}*", keyword))), Occur.SHOULD);
                query.Add(new FuzzyQuery(new Term("title", keyword)), Occur.SHOULD);
                query.Add(new WildcardQuery(new Term("full_content", String.Format("*{0}*", keyword))), Occur.SHOULD);
            }
            else
            {
                #region First Approach by building Query String of Lucene
                //StringBuilder sb = new StringBuilder();
                //String[] tokens = keyword.Split(' ');
                //for (int i = 0; i < tokens.Length; i++)
                //{
                //    String token = QueryParser.Escape(tokens[i]);                    
                //    //Exclude AND/OR from the keyword and process as user given query
                //    if (token == "OR" || token == "AND")
                //    {
                //        if (sb.Length > 0)
                //            sb.AppendFormat(" {0}", token);   ///Appending AND/OR
                //    }
                //    else
                //    {
                //        if (sb.Length == 0)
                //            sb.AppendFormat("{0}", token);
                //        else
                //        {
                //            if (sb.ToString().EndsWith(" OR") || sb.ToString().EndsWith(" AND"))
                //                sb.AppendFormat(" {0}", token);
                //            else
                //                sb.AppendFormat(" AND {0}", token);             
                //        }
                //    }
                //}
                ////Build the AND Clause
                //query.Add(queryParser.Parse(sb.ToString()), Occur.SHOULD);
                #endregion

                #region Building With WildCard Query
                if (keyword.IndexOf(" OR ") > 0)
                {
                    queryParser.DefaultOperator = QueryParser.Operator.OR;
                    query.Add(queryParser.Parse(keyword), Occur.SHOULD);
                }
                else if (keyword.IndexOf(" AND ") > 0)
                {
                    queryParser.DefaultOperator = QueryParser.Operator.AND;
                    query.Add(queryParser.Parse(keyword), Occur.SHOULD);
                }
                else
                {
                    String[] tokens = keyword.Split(' ');
                    for (int i = 0; i < tokens.Length; i++)
                    {
                        String token = CleanKeywordToken(tokens[i]);
                        //if(i == 0)
                        //    query.Add(new FuzzyQuery(new Term("full_content", keyword)), Occur.SHOULD);
                        query.Add(new WildcardQuery(new Term("full_content", String.Format("*{0}*", token))), Occur.MUST);
                    }
                }
                #endregion

                #region The Easiest Approach with QueryParser
                //keyword = QueryParser.Escape(keyword);
                //query.Add(queryParser.Parse(keyword), Occur.SHOULD);

                //Build the OR Clause
                //Giving Lots of Matches and showing irrelevent results before most relevent result. So commenting it out.
                //query.Add(queryParser.Parse(sb.ToString().Replace(" AND ", " OR ")), Occur.SHOULD);
                #endregion                
            }
        }

        private string CleanKeywordToken(string token)
        {
            token = token.Replace("\"", "");
            return QueryParser.Escape(token);
        }
        /// <summary>
        /// Determines whether a user given search keyword can be considered as exact search
        /// </summary>
        /// <param name="keyword"></param>
        /// <returns></returns>
        private bool IsExactSearch(ref string keyword)
        {
            if (keyword.StartsWith("\"") && keyword.EndsWith("\""))
            {
                if (keyword.IndexOf('"', 1, keyword.Length - 2) > 0) ///There is an intermediat double quote in between
                {
                    keyword = keyword.Substring(1, keyword.Length - 2);
                    return false;
                }
                return true;
            }
            return false;
        }

        #region Obsolete Queries
        /// <summary>
        /// <code>Obsolete</code> Works like contains query. Probably suitable in auto complete feature.
        /// </summary>
        /// <param name="keyword"></param>
        /// <param name="pageNo"></param>
        /// <param name="pageSize"></param>
        /// <returns>Returns a Comma Separated String of Product IDs</returns>
        public String PrefixSearch(String keyword, int pageNo, int pageSize)
        {   
            Query prefixQuery = new PrefixQuery(new Lucene.Net.Index.Term("title", QueryParser.Escape(keyword)));
            
            ///Perform Search
            TopDocs docs = _IndexSearcher.Search(prefixQuery, pageNo * pageSize); //_IndexSearcher.Search(query, pageSize * pageNo);
            int totalRecord = docs.TotalHits;
            return GetProductIDs(docs, pageNo, pageSize);
        }
        /// <summary>
        /// <code>Obsolete</code> Example of Wildcard Search. 
        /// </summary>
        /// <param name="keyword"></param>
        /// <param name="pageNo"></param>
        /// <param name="pageSize"></param>
        /// <returns>Returns a Comma Separated String of Product IDs</returns>
        public String WildcardSearch(String keyword, int pageNo, int pageSize)
        {
            Query wildCardQuery = new WildcardQuery(new Lucene.Net.Index.Term("title", "*" + keyword + "*"));

            ///Perform Search
            TopDocs docs = _IndexSearcher.Search(wildCardQuery, pageNo * pageSize); //_IndexSearcher.Search(query, pageSize * pageNo);
            int totalRecord = docs.TotalHits;
            return GetProductIDs(docs, pageNo, pageSize);
        }
        #endregion

        /// <summary>
        /// Optimized for Search Suggestion. Searches only in Product title and Model
        /// </summary>
        /// <param name="keyword"></param>
        /// <param name="pageNo"></param>
        /// <param name="pageSize"></param>
        /// <returns>Returns a Collection containing Product ID and Title, directly from the Lucene index documents</returns>
        public List<ProductResult> AutoCompleateQuery(String keyword, int pageNo, int pageSize)
        {
            BooleanQuery query = new BooleanQuery();

            #region First Approach
            //keyword = QueryParser.Escape(keyword).ToUpper();
            //QueryParser qp = new QueryParser(Lucene.Net.Util.Version.LUCENE_30, "title", new WhitespaceAnalyzer());
            //if (keyword.WordCount() == 1)
            //{
            //    query.Add(new WildcardQuery(new Term("model", String.Format("*{0}*", keyword))), Occur.SHOULD);
            //    query.Add(new WildcardQuery(new Term("title", String.Format("*{0}*", keyword))), Occur.SHOULD);
            //    query.Add(new FuzzyQuery(new Term("title", keyword)), Occur.SHOULD);
            //}
            //else
            //{
            //    query.Add(qp.Parse(keyword), Occur.SHOULD);
            //}
            #endregion

            #region Second Approach 
            ///This approach is done according to the Original BuildQuery method, with a signle difference that,
            ///Autocomplete only searches few important fields not all, for lightweight query result
            //String[] tokens = keyword.Split(' ');
            //for (int i = 0; i < tokens.Length; i++)
            //{
            //    String token = QueryParser.Escape(tokens[i]);
            //    query.Add(new WildcardQuery(new Term("model", String.Format("*{0}*", token))), Occur.SHOULD);
            //    query.Add(new WildcardQuery(new Term("title", String.Format("*{0}*", token))), Occur.SHOULD);
            //    query.Add(new FuzzyQuery(new Term("title", token)), Occur.SHOULD);
            //}            
            #endregion

            BuildQuery(keyword, query);
            ///Perform Search
            TopDocs docs = _IndexSearcher.Search(query, pageNo * pageSize); 
            int totalRecord = docs.TotalHits;
            return GetProducts(docs, pageNo, pageSize);
        }        
        #endregion Public Methods

        #region Private Methods
        /// <summary>
        /// Serializes Lucene TopDocs to Comma Separated Product IDs.
        /// </summary>
        /// <param name="topDocs"></param>
        /// <returns></returns>
        private String GetProductIDs(TopDocs topDocs, int pageNo, int pageSize)
        {
            StringBuilder sb = new StringBuilder(10);
            int startIndex = (pageNo * pageSize) - (pageSize);
            int endIndex = (pageNo * pageSize) - 1;
            int scoreDocCount = topDocs.ScoreDocs.Count();
            for (int i = startIndex; i < scoreDocCount; i++) 
            {
                ScoreDoc match = topDocs.ScoreDocs[i];
                Document doc = _IndexSearcher.Doc(match.Doc);
                if (sb.Length == 0)
                    sb.Append(doc.Get("id"));
                else
                    sb.AppendFormat(",{0}", doc.Get("id"));                
            }
            return sb.ToString();
        }
        /// <summary>
        /// Builds a product result collection by reading field data from Lucene documents
        /// </summary>
        /// <param name="topDocs"></param>
        /// <param name="pageNo"></param>
        /// <param name="pageSize"></param>
        /// <returns></returns>
        private List<ProductResult> GetProducts(TopDocs topDocs, int pageNo, int pageSize)
        {
            int startIndex = (pageNo * pageSize) - (pageSize);
            int endIndex = (pageNo * pageSize) - 1;
            int scoreDocCount = topDocs.ScoreDocs.Count();
            List<ProductResult> result = new List<ProductResult>();
            for (int i = startIndex; i < scoreDocCount; i++)
            {
                ScoreDoc match = topDocs.ScoreDocs[i];
                Document doc = _IndexSearcher.Doc(match.Doc);
                ProductResult product = new ProductResult(){
                    ID = doc.Get("id").ToInt(),
                    Title = doc.Get("title_orig")
                };
                result.Add(product);
            }
            return result;
        }
        #endregion Private Methods        
    }
}
